package com.fa4j.common.web.aspect;

import com.alibaba.fastjson2.JSON;
import com.fa4j.common.base.exception.BaseException;
import com.fa4j.common.base.exception.ServerException;
import com.fa4j.common.base.model.AbstractCmd;
import com.fa4j.common.web.config.CommonWebPropsConfig;
import com.fa4j.common.web.model.ClientInfo;
import com.fa4j.common.web.model.CommonWebConst;
import com.fa4j.common.web.model.LogInfo;
import com.fa4j.common.web.util.ContextUtil;
import com.fa4j.common.web.util.RequestUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.InputStreamSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ObjectUtils;

import java.util.List;
import java.util.Optional;

import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;


@Slf4j
@Aspect
@Order(Integer.MIN_VALUE)
@RequiredArgsConstructor
public class CommonWebAspect {

    private final CommonWebPropsConfig config;
    private final ApplicationContext applicationContext;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Around("@within(org.springframework.stereotype.Controller)"
            + "||@within(org.springframework.web.bind.annotation.RestController)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Object result;
        LogInfo logInfo = null;
        try {
            logInfo = initLogInfo(point); // 初始化日志信息
            fillOperatorId(point.getArgs());//填充参数操作人信息
        } catch (Exception e) {
            log.error("初始化日志信息异常:{}", point, e);
        }
        if (logInfo == null || isExclude(config.getLog().getExcludes(), logInfo)) {
            return point.proceed();
        }
        try {
            result = point.proceed();
            logInfo.setResult(result);
        } catch (Throwable throwable) {
            logInfo.setThrowable(throwable);
            throw throwable;
        } finally {
            logInfo.setEndTime(System.currentTimeMillis());
            try {
                printLog(point, logInfo);//打印日志
                applicationContext.publishEvent(logInfo);
            } catch (Exception e) {
                log.error("处理日志异常", e);
            }
        }
        return result;
    }

    /**
     * 填充参数操作人信息
     *
     * @param args 方法参数
     */
    private void fillOperatorId(Object[] args) {
        Long operatorId = ContextUtil.get(CommonWebConst.OPERATOR_ID);
        if (operatorId == null) {
            return;
        }
        for (Object arg : args) {
            if (arg instanceof AbstractCmd cmd) {
                cmd.setOperatorId(operatorId);
            }
        }
    }


    private void printLog(ProceedingJoinPoint point, LogInfo logInfo) {
        ContextUtil.set(CommonWebConst.PRINT_LOG_FLAG, "true");
        Logger logger = LoggerFactory.getLogger(point.getTarget().getClass());
        String logMsg = "[%s:%s][cost:%sms]request:%s"
                .formatted(logInfo.getHttpMethod(), logInfo.getHttpPath(),
                        (logInfo.getEndTime() - logInfo.getStartTime()),
                        JSON.toJSONString(logInfo.getParams()));
        if (!ObjectUtils.isEmpty(logInfo.getServiceFrom())) {
            logMsg = "(" + logInfo.getServiceFrom() + ")" + logMsg;
        }
        if (logInfo.getThrowable() != null) {
            if (logInfo.getThrowable() instanceof BaseException e && !(logInfo.getThrowable() instanceof ServerException)) {
                logger.warn("{},error:{}-{}", logMsg, e.getErrorType().getErrCode(), e.getMessage());
            } else {
                logger.error("{}", logMsg, logInfo.getThrowable());
            }
        } else {
            if (logInfo.getMethod().getReturnType() == Void.class) {
                if ("GET".equals(logInfo.getHttpMethod())) {
                    logger.debug("{}", logMsg);
                } else {
                    logger.info("{}", logMsg);
                }
            } else {
                if ("GET".equals(logInfo.getHttpMethod())) {
                    logger.debug("{},response:{}", logMsg, JSON.toJSONString(logInfo.getResult()));
                } else {
                    logger.info("{},response:{}", logMsg, JSON.toJSONString(logInfo.getResult()));
                }

            }
        }

    }

    private boolean isExclude(List<String> excludes, LogInfo logInfo) {
        if (excludes != null && !excludes.isEmpty()) {
            for (String exclude : excludes) {
                if (antPathMatcher.match(exclude, logInfo.getHttpMethod() + ":" + logInfo.getHttpPath())) {
                    return true;
                }
            }
        }
        return false;
    }

    private LogInfo initLogInfo(ProceedingJoinPoint point) {
        if (point.getSignature() instanceof MethodSignature methodSignature
                && RequestUtil.isRequestMethod(methodSignature.getMethod())
        ) {
            HttpServletRequest request = RequestUtil.getRequest();
            LogInfo info = new LogInfo();
            info.setStartTime(System.currentTimeMillis());
            String path = Optional.ofNullable((String) request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).orElse(request.getRequestURI());
            info.setHttpPath(path);
            info.setHttpMethod(request.getMethod());
            info.setClientInfo(ContextUtil.get(ClientInfo.class));
            info.setOperatorId(ContextUtil.get(CommonWebConst.OPERATOR_ID));
            info.setMethod(methodSignature.getMethod());
            info.setServiceFrom(request.getHeader(CommonWebConst.HEADER_SERVICE_FROM));
            String[] parameterNames = methodSignature.getParameterNames();
            if (parameterNames != null) {
                for (int i = 0; i < parameterNames.length; i++) {
                    Object value = point.getArgs()[i];
                    if (value instanceof InputStreamSource
                            || value instanceof ServletResponse
                            || value instanceof ServletRequest
                    ) {
                        value = "Object[" + value.getClass().getSimpleName() + "]";
                    }
                    info.getParams().put(parameterNames[i], value);
                    if (info.getOperatorId() == null && value instanceof AbstractCmd cmd) {
                        info.setOperatorId(cmd.getOperatorId());
                    }
                }
            }
            ContextUtil.set(info);
            return info;
        }
        return null;
    }
}
